/*
 * Copyright (c) 2007, 2015, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
/*
 * Copyright 2005 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.org.apache.xerces.internal.jaxp.validation;

import java.io.IOException;
import java.util.Enumeration;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;

import com.sun.org.apache.xerces.internal.impl.Constants;
import com.sun.org.apache.xerces.internal.impl.XMLErrorReporter;
import com.sun.org.apache.xerces.internal.impl.validation.EntityState;
import com.sun.org.apache.xerces.internal.impl.validation.ValidationManager;
import com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator;
import com.sun.org.apache.xerces.internal.impl.xs.util.SimpleLocator;
import com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl;
import com.sun.org.apache.xerces.internal.util.NamespaceSupport;
import com.sun.org.apache.xerces.internal.util.SymbolTable;
import com.sun.org.apache.xerces.internal.util.XMLAttributesImpl;
import com.sun.org.apache.xerces.internal.util.XMLSymbols;
import com.sun.org.apache.xerces.internal.xni.NamespaceContext;
import com.sun.org.apache.xerces.internal.xni.QName;
import com.sun.org.apache.xerces.internal.xni.XMLString;
import com.sun.org.apache.xerces.internal.xni.XNIException;
import com.sun.org.apache.xerces.internal.xni.parser.XMLParseException;
import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Entity;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;
import org.xml.sax.SAXException;

/**
 * <p>A validator helper for <code>DOMSource</code>s.</p>
 *
 * @author Michael Glavassevich, IBM
 * @version $Id: DOMValidatorHelper.java,v 1.9 2010-11-01 04:40:08 joehw Exp $
 */
final class DOMValidatorHelper implements ValidatorHelper, EntityState {

  //
  // Constants
  //

  /**
   * Chunk size (1024).
   */
  private static final int CHUNK_SIZE = (1 << 10);

  /**
   * Chunk mask (CHUNK_SIZE - 1).
   */
  private static final int CHUNK_MASK = CHUNK_SIZE - 1;

  // property identifiers

  /**
   * Property identifier: error reporter.
   */
  private static final String ERROR_REPORTER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.ERROR_REPORTER_PROPERTY;

  /**
   * Property identifier: namespace context.
   */
  private static final String NAMESPACE_CONTEXT =
      Constants.XERCES_PROPERTY_PREFIX + Constants.NAMESPACE_CONTEXT_PROPERTY;

  /**
   * Property identifier: XML Schema validator.
   */
  private static final String SCHEMA_VALIDATOR =
      Constants.XERCES_PROPERTY_PREFIX + Constants.SCHEMA_VALIDATOR_PROPERTY;

  /**
   * Property identifier: symbol table.
   */
  private static final String SYMBOL_TABLE =
      Constants.XERCES_PROPERTY_PREFIX + Constants.SYMBOL_TABLE_PROPERTY;

  /**
   * Property identifier: validation manager.
   */
  private static final String VALIDATION_MANAGER =
      Constants.XERCES_PROPERTY_PREFIX + Constants.VALIDATION_MANAGER_PROPERTY;

  //
  // Data
  //

  /**
   * Error reporter.
   */
  private XMLErrorReporter fErrorReporter;

  /**
   * The namespace context of this document: stores namespaces in scope.
   **/
  private NamespaceSupport fNamespaceContext;

  /**
   * The namespace context of the DOMSource, includes context from ancestor nodes.
   **/
  private DOMNamespaceContext fDOMNamespaceContext = new DOMNamespaceContext();

  /**
   * Schema validator.
   **/
  private XMLSchemaValidator fSchemaValidator;

  /**
   * Symbol table
   **/
  private SymbolTable fSymbolTable;

  /**
   * Validation manager.
   **/
  private ValidationManager fValidationManager;

  /**
   * Component manager.
   **/
  private XMLSchemaValidatorComponentManager fComponentManager;

  /**
   * Simple Locator.
   **/
  private final SimpleLocator fXMLLocator = new SimpleLocator(null, null, -1, -1, -1);

  /**
   * DOM document handler.
   **/
  private DOMDocumentHandler fDOMValidatorHandler;

  /**
   * DOM result augmentor.
   **/
  private final DOMResultAugmentor fDOMResultAugmentor = new DOMResultAugmentor(this);

  /**
   * DOM result builder.
   **/
  private final DOMResultBuilder fDOMResultBuilder = new DOMResultBuilder();

  /**
   * Map for tracking unparsed entities.
   **/
  private NamedNodeMap fEntities = null;

  /**
   * Array for holding character data.
   **/
  private char[] fCharBuffer = new char[CHUNK_SIZE];

  /**
   * Root node.
   **/
  private Node fRoot;

  /**
   * Current element.
   **/
  private Node fCurrentElement;

  /**
   * Fields for start element, end element and characters.
   **/
  final QName fElementQName = new QName();
  final QName fAttributeQName = new QName();
  final XMLAttributesImpl fAttributes = new XMLAttributesImpl();
  final XMLString fTempString = new XMLString();

  public DOMValidatorHelper(XMLSchemaValidatorComponentManager componentManager) {
    fComponentManager = componentManager;
    fErrorReporter = (XMLErrorReporter) fComponentManager.getProperty(ERROR_REPORTER);
    fNamespaceContext = (NamespaceSupport) fComponentManager.getProperty(NAMESPACE_CONTEXT);
    fSchemaValidator = (XMLSchemaValidator) fComponentManager.getProperty(SCHEMA_VALIDATOR);
    fSymbolTable = (SymbolTable) fComponentManager.getProperty(SYMBOL_TABLE);
    fValidationManager = (ValidationManager) fComponentManager.getProperty(VALIDATION_MANAGER);
  }

    /*
     * ValidatorHelper methods
     */

  public void validate(Source source, Result result)
      throws SAXException, IOException {
    if (result instanceof DOMResult || result == null) {
      final DOMSource domSource = (DOMSource) source;
      final DOMResult domResult = (DOMResult) result;
      Node node = domSource.getNode();
      fRoot = node;
      if (node != null) {
        fComponentManager.reset();
        fValidationManager.setEntityState(this);
        fDOMNamespaceContext.reset();
        String systemId = domSource.getSystemId();
        fXMLLocator.setLiteralSystemId(systemId);
        fXMLLocator.setExpandedSystemId(systemId);
        fErrorReporter.setDocumentLocator(fXMLLocator);
        try {
          // regardless of what type of node this is, fire start and end document events
          setupEntityMap((node.getNodeType() == Node.DOCUMENT_NODE) ? (Document) node
              : node.getOwnerDocument());
          setupDOMResultHandler(domSource, domResult);
          fSchemaValidator.startDocument(fXMLLocator, null, fDOMNamespaceContext, null);
          validate(node);
          fSchemaValidator.endDocument(null);
        } catch (XMLParseException e) {
          throw Util.toSAXParseException(e);
        } catch (XNIException e) {
          throw Util.toSAXException(e);
        } finally {
          // Release references to application objects
          fRoot = null;
          //fCurrentElement = null; -- keep the reference to support current-element-node property
          fEntities = null;
          if (fDOMValidatorHandler != null) {
            fDOMValidatorHandler.setDOMResult(null);
          }
        }
      }
      return;
    }
    throw new IllegalArgumentException(
        JAXPValidationMessageFormatter.formatMessage(fComponentManager.getLocale(),
            "SourceResultMismatch",
            new Object[]{source.getClass().getName(), result.getClass().getName()}));
  }

    /*
     * EntityState methods
     */

  public boolean isEntityDeclared(String name) {
    return false;
  }

  public boolean isEntityUnparsed(String name) {
    if (fEntities != null) {
      Entity entity = (Entity) fEntities.getNamedItem(name);
      if (entity != null) {
        return (entity.getNotationName() != null);
      }
    }
    return false;
  }

    /*
     * Other methods
     */

  /**
   * Traverse the DOM and fire events to the schema validator.
   */
  private void validate(Node node) {
    final Node top = node;
    // Performs a non-recursive traversal of the DOM. This
    // will avoid a stack overflow for DOMs with high depth.
    while (node != null) {
      beginNode(node);
      Node next = node.getFirstChild();
      while (next == null) {
        finishNode(node);
        if (top == node) {
          break;
        }
        next = node.getNextSibling();
        if (next == null) {
          node = node.getParentNode();
          if (node == null || top == node) {
            if (node != null) {
              finishNode(node);
            }
            next = null;
            break;
          }
        }
      }
      node = next;
    }
  }

  /**
   * Do processing for the start of a node.
   */
  private void beginNode(Node node) {
    switch (node.getNodeType()) {
      case Node.ELEMENT_NODE:
        fCurrentElement = node;
        // push namespace context
        fNamespaceContext.pushContext();
        // start element
        fillQName(fElementQName, node);
        processAttributes(node.getAttributes());
        fSchemaValidator.startElement(fElementQName, fAttributes, null);
        break;
      case Node.TEXT_NODE:
        if (fDOMValidatorHandler != null) {
          fDOMValidatorHandler.setIgnoringCharacters(true);
          sendCharactersToValidator(node.getNodeValue());
          fDOMValidatorHandler.setIgnoringCharacters(false);
          fDOMValidatorHandler.characters((Text) node);
        } else {
          sendCharactersToValidator(node.getNodeValue());
        }
        break;
      case Node.CDATA_SECTION_NODE:
        if (fDOMValidatorHandler != null) {
          fDOMValidatorHandler.setIgnoringCharacters(true);
          fSchemaValidator.startCDATA(null);
          sendCharactersToValidator(node.getNodeValue());
          fSchemaValidator.endCDATA(null);
          fDOMValidatorHandler.setIgnoringCharacters(false);
          fDOMValidatorHandler.cdata((CDATASection) node);
        } else {
          fSchemaValidator.startCDATA(null);
          sendCharactersToValidator(node.getNodeValue());
          fSchemaValidator.endCDATA(null);
        }
        break;
      case Node.PROCESSING_INSTRUCTION_NODE:
        /**
         * The validator does nothing with processing instructions so bypass it.
         * Send the ProcessingInstruction node directly to the result builder.
         */
        if (fDOMValidatorHandler != null) {
          fDOMValidatorHandler.processingInstruction((ProcessingInstruction) node);
        }
        break;
      case Node.COMMENT_NODE:
        /**
         * The validator does nothing with comments so bypass it.
         * Send the Comment node directly to the result builder.
         */
        if (fDOMValidatorHandler != null) {
          fDOMValidatorHandler.comment((Comment) node);
        }
        break;
      case Node.DOCUMENT_TYPE_NODE:
        /**
         * Send the DocumentType node directly to the result builder.
         */
        if (fDOMValidatorHandler != null) {
          fDOMValidatorHandler.doctypeDecl((DocumentType) node);
        }
        break;
      default: // Ignore other node types.
        break;
    }
  }

  /**
   * Do processing for the end of a node.
   */
  private void finishNode(Node node) {
    if (node.getNodeType() == Node.ELEMENT_NODE) {
      fCurrentElement = node;
      // end element
      fillQName(fElementQName, node);
      fSchemaValidator.endElement(fElementQName, null);
      // pop namespace context
      fNamespaceContext.popContext();
    }
  }

  /**
   * Extracts NamedNodeMap of entities. We need this to validate
   * elements and attributes of type xs:ENTITY, xs:ENTITIES or
   * types dervied from them.
   */
  private void setupEntityMap(Document doc) {
    if (doc != null) {
      DocumentType docType = doc.getDoctype();
      if (docType != null) {
        fEntities = docType.getEntities();
        return;
      }
    }
    fEntities = null;
  }

  /**
   * Sets up handler for <code>DOMResult</code>.
   */
  private void setupDOMResultHandler(DOMSource source, DOMResult result) throws SAXException {
    // If there's no DOMResult, unset the validator handler
    if (result == null) {
      fDOMValidatorHandler = null;
      fSchemaValidator.setDocumentHandler(null);
      return;
    }
    final Node nodeResult = result.getNode();
    // If the source node and result node are the same use the DOMResultAugmentor.
    // Otherwise use the DOMResultBuilder.
    if (source.getNode() == nodeResult) {
      fDOMValidatorHandler = fDOMResultAugmentor;
      fDOMResultAugmentor.setDOMResult(result);
      fSchemaValidator.setDocumentHandler(fDOMResultAugmentor);
      return;
    }
    if (result.getNode() == null) {
      try {
        DocumentBuilderFactory factory =
            fComponentManager.getFeature(Constants.ORACLE_FEATURE_SERVICE_MECHANISM) ?
                DocumentBuilderFactory.newInstance() : new DocumentBuilderFactoryImpl();
        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        result.setNode(builder.newDocument());
      } catch (ParserConfigurationException e) {
        throw new SAXException(e);
      }
    }
    fDOMValidatorHandler = fDOMResultBuilder;
    fDOMResultBuilder.setDOMResult(result);
    fSchemaValidator.setDocumentHandler(fDOMResultBuilder);
  }

  private void fillQName(QName toFill, Node node) {
    final String prefix = node.getPrefix();
    final String localName = node.getLocalName();
    final String rawName = node.getNodeName();
    final String namespace = node.getNamespaceURI();

    toFill.uri =
        (namespace != null && namespace.length() > 0) ? fSymbolTable.addSymbol(namespace) : null;
    toFill.rawname = (rawName != null) ? fSymbolTable.addSymbol(rawName) : XMLSymbols.EMPTY_STRING;

    // Is this a DOM level1 document?
    if (localName == null) {
      int k = rawName.indexOf(':');
      if (k > 0) {
        toFill.prefix = fSymbolTable.addSymbol(rawName.substring(0, k));
        toFill.localpart = fSymbolTable.addSymbol(rawName.substring(k + 1));
      } else {
        toFill.prefix = XMLSymbols.EMPTY_STRING;
        toFill.localpart = toFill.rawname;
      }
    } else {
      toFill.prefix = (prefix != null) ? fSymbolTable.addSymbol(prefix) : XMLSymbols.EMPTY_STRING;
      toFill.localpart =
          (localName != null) ? fSymbolTable.addSymbol(localName) : XMLSymbols.EMPTY_STRING;
    }
  }

  private void processAttributes(NamedNodeMap attrMap) {
    final int attrCount = attrMap.getLength();
    fAttributes.removeAllAttributes();
    for (int i = 0; i < attrCount; ++i) {
      Attr attr = (Attr) attrMap.item(i);
      String value = attr.getValue();
      if (value == null) {
        value = XMLSymbols.EMPTY_STRING;
      }
      fillQName(fAttributeQName, attr);
      // REVISIT: Assuming all attributes are of type CDATA. The actual type may not matter. -- mrglavas
      fAttributes.addAttributeNS(fAttributeQName, XMLSymbols.fCDATASymbol, value);
      fAttributes.setSpecified(i, attr.getSpecified());
      // REVISIT: Should we be looking at non-namespace attributes
      // for additional mappings? Should we detect illegal namespace
      // declarations and exclude them from the context? -- mrglavas
      if (fAttributeQName.uri == NamespaceContext.XMLNS_URI) {
        // process namespace attribute
        if (fAttributeQName.prefix == XMLSymbols.PREFIX_XMLNS) {
          fNamespaceContext.declarePrefix(fAttributeQName.localpart,
              value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
        } else {
          fNamespaceContext.declarePrefix(XMLSymbols.EMPTY_STRING,
              value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
        }
      }
    }
  }

  private void sendCharactersToValidator(String str) {
    if (str != null) {
      final int length = str.length();
      final int remainder = length & CHUNK_MASK;
      if (remainder > 0) {
        str.getChars(0, remainder, fCharBuffer, 0);
        fTempString.setValues(fCharBuffer, 0, remainder);
        fSchemaValidator.characters(fTempString, null);
      }
      int i = remainder;
      while (i < length) {
        str.getChars(i, i += CHUNK_SIZE, fCharBuffer, 0);
        fTempString.setValues(fCharBuffer, 0, CHUNK_SIZE);
        fSchemaValidator.characters(fTempString, null);
      }
    }
  }

  Node getCurrentElement() {
    return fCurrentElement;
  }

  /**
   * NamespaceContext for the DOMSource, includes context for ancestor nodes.
   */
  final class DOMNamespaceContext implements NamespaceContext {

    //
    // Data
    //

    /**
     * Namespace binding information. This array is composed of a
     * series of tuples containing the namespace binding information:
     * &lt;prefix, uri&gt;.
     */
    protected String[] fNamespace = new String[16 * 2];

    /**
     * The size of the namespace information array.
     */
    protected int fNamespaceSize = 0;

    /**
     * Flag indicating whether the namespace context
     * has been from the root node's ancestors.
     */
    protected boolean fDOMContextBuilt = false;

    //
    // Methods
    //

    public void pushContext() {
      fNamespaceContext.pushContext();
    }

    public void popContext() {
      fNamespaceContext.popContext();
    }

    public boolean declarePrefix(String prefix, String uri) {
      return fNamespaceContext.declarePrefix(prefix, uri);
    }

    public String getURI(String prefix) {
      String uri = fNamespaceContext.getURI(prefix);
      if (uri == null) {
        if (!fDOMContextBuilt) {
          fillNamespaceContext();
          fDOMContextBuilt = true;
        }
        if (fNamespaceSize > 0 &&
            !fNamespaceContext.containsPrefix(prefix)) {
          uri = getURI0(prefix);
        }
      }
      return uri;
    }

    public String getPrefix(String uri) {
      return fNamespaceContext.getPrefix(uri);
    }

    public int getDeclaredPrefixCount() {
      return fNamespaceContext.getDeclaredPrefixCount();
    }

    public String getDeclaredPrefixAt(int index) {
      return fNamespaceContext.getDeclaredPrefixAt(index);
    }

    public Enumeration getAllPrefixes() {
      return fNamespaceContext.getAllPrefixes();
    }

    public void reset() {
      fDOMContextBuilt = false;
      fNamespaceSize = 0;
    }

    private void fillNamespaceContext() {
      if (fRoot != null) {
        Node currentNode = fRoot.getParentNode();
        while (currentNode != null) {
          if (Node.ELEMENT_NODE == currentNode.getNodeType()) {
            NamedNodeMap attributes = currentNode.getAttributes();
            final int attrCount = attributes.getLength();
            for (int i = 0; i < attrCount; ++i) {
              Attr attr = (Attr) attributes.item(i);
              String value = attr.getValue();
              if (value == null) {
                value = XMLSymbols.EMPTY_STRING;
              }
              fillQName(fAttributeQName, attr);
              // REVISIT: Should we be looking at non-namespace attributes
              // for additional mappings? Should we detect illegal namespace
              // declarations and exclude them from the context? -- mrglavas
              if (fAttributeQName.uri == NamespaceContext.XMLNS_URI) {
                // process namespace attribute
                if (fAttributeQName.prefix == XMLSymbols.PREFIX_XMLNS) {
                  declarePrefix0(fAttributeQName.localpart,
                      value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
                } else {
                  declarePrefix0(XMLSymbols.EMPTY_STRING,
                      value.length() != 0 ? fSymbolTable.addSymbol(value) : null);
                }
              }
            }

          }
          currentNode = currentNode.getParentNode();
        }
      }
    }

    private void declarePrefix0(String prefix, String uri) {
      // resize array, if needed
      if (fNamespaceSize == fNamespace.length) {
        String[] namespacearray = new String[fNamespaceSize * 2];
        System.arraycopy(fNamespace, 0, namespacearray, 0, fNamespaceSize);
        fNamespace = namespacearray;
      }

      // bind prefix to uri in current context
      fNamespace[fNamespaceSize++] = prefix;
      fNamespace[fNamespaceSize++] = uri;
    }

    private String getURI0(String prefix) {
      // find prefix in the DOM context
      for (int i = 0; i < fNamespaceSize; i += 2) {
        if (fNamespace[i] == prefix) {
          return fNamespace[i + 1];
        }
      }
      // prefix not found
      return null;
    }
  }

} // DOMValidatorHelper
